﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace N3P.WCF.Proxyless.FullDuplex.Silverlight
{
    /// <summary>
    /// Helper class for working with <see cref="Message"/> objects
    /// </summary>
    internal static class MessageHelper
    {

        /// <summary>
        /// The <see cref="Type"/> vs <see cref="DataContractSerializer"/>
        /// </summary>
        private static readonly Dictionary<Type, DataContractSerializer> DataContractSerializers = new Dictionary<Type, DataContractSerializer>();
        /// <summary>
        /// Prefix for data contract xml namespaces
        /// </summary>
        const string DcNsPrefix = "http://schemas.datacontract.org/2004/07/";
        /// <summary>
        /// The cache of types known to be data contracts or not
        /// </summary>
        private static readonly Dictionary<Type, bool> KnownTypeNatures = new Dictionary<Type, bool>();
        /// <summary>
        /// The <see cref="string"/> (SOAP action) vs caller cache
        /// </summary>
        private static readonly Dictionary<string, Action<Message>> MessageDeserializers = new Dictionary<string, Action<Message>>();
        /// <summary>
        /// The <see cref="MethodInfo"/> vs message serializer cachce
        /// </summary>
        private static readonly Dictionary<MethodInfo, Func<object[], Message>> MessageSerializers = new Dictionary<MethodInfo, Func<object[], Message>>();
        /// <summary>
        /// The <see cref="Type"/> vs <see cref="XmlSerializer"/> cache
        /// </summary>
        private static readonly Dictionary<Type, XmlSerializer> XmlSerializers = new Dictionary<Type, XmlSerializer>();
        /// <summary>
        /// XML-NS xsi
        /// </summary>
        private const string Xsi = "http://www.w3.org/2001/XMLSchema-instance";




        /// <summary>
        /// Deserializes a data contract
        /// </summary>
        /// <param name="to">The type the data should be deserialized to</param>
        /// <param name="data">The <see cref="XElement"/> that needs to be deserialized</param>
        /// <returns>The deserialized object</returns>
        private static object DeserializeDataContract(Type to, XElement data)
        {
            var serializer = GetDataContractSerializer(to);
            var newNs = 
                data.Attributes().First(
                    x =>
                    x.Name.NamespaceName.IndexOf("/xmlns/", StringComparison.InvariantCulture) > -1 &&
                    x.Value.IndexOf("XMLSchema-instance", StringComparison.InvariantCultureIgnoreCase) == -1);

            var innerData = data.Elements().Cast<object>().ToArray();
            var newBody = new XElement(XName.Get(to.Name, newNs.Value));
            newBody.Add(innerData);

            var nilAttribute = data.Attributes().FirstOrDefault(x => x.Name.LocalName == "nil");
            
            if(nilAttribute != null && bool.Parse(nilAttribute.Value))
            {
                return null;
            }

            return serializer.ReadObject(newBody.CreateReader());
        }

        /// <summary>
        /// Deserializes a primitive (more accurately a non-data contract)
        /// </summary>
        /// <param name="to">The type the data should be deserialized to</param>
        /// <param name="data">The string representation of the <see cref="XElement"/> that needs to be deserialized</param>
        /// <returns>The deserialized object</returns>
        private static object DeserializePrimitive(Type to, string data)
        {
            var serializer = GetXmlSerializer(to);
            return serializer.Deserialize(new StringReader(data));
        }

        /// <summary>
        /// Gets the <see cref="DataContractSerializer"/> capable of handling the specified <see cref="Type"/>
        /// </summary>
        /// <param name="type">The <see cref="Type"/> the serializer must be able to handle</param>
        /// <returns>A <see cref="DataContractSerializer"/> capable of handling the specified <see cref="Type"/></returns>
        private static DataContractSerializer GetDataContractSerializer(Type type)
        {
            if (!DataContractSerializers.ContainsKey(type))
            {
                return DataContractSerializers[type] = new DataContractSerializer(type);
            }

            return DataContractSerializers[type];
        }

        /// <summary>
        /// Gets the <see cref="XElement"/> objects that make up the parameters to the method call the <see cref="Message"/> requests
        /// </summary>
        /// <param name="message">The <see cref="Message"/> to get the method's parameters from</param>
        /// <param name="action">The name of the method whose call's parameters should be gotten</param>
        /// <returns>An <see cref="IEnumerable{T}"/> of <see cref="XElement"/> containing the ordered parameters for the method call</returns>
        private static IEnumerable<XElement> GetMethodParameters(Message message, string action)
        {
            var rawMessage = message.ToString();
            var stream = new MemoryStream();
            var bytes = Encoding.Unicode.GetBytes(rawMessage);
            stream.Write(bytes, 0, bytes.Length);
            stream.Position = 0;
            var doc = XDocument.Load(stream);
            return doc.Document.Elements()
                      .SelectMany(x => x.Elements())
                      .First(x => x.Name.LocalName == "Body")
                      .Elements().First(x => x.Name.LocalName == action)
                      .Elements();
        }

        /// <summary>
        /// Gets an <see cref="XmlSerializer"/> capable of handling the specified type
        /// </summary>
        /// <param name="type">The type to get the serializer for</param>
        /// <returns>An <see cref="XmlSerializer"/> capable of handling the specified type</returns>
        private static XmlSerializer GetXmlSerializer(Type type)
        {
            if (!XmlSerializers.ContainsKey(type))
            {
                return XmlSerializers[type] = new XmlSerializer(type);
            }

            return XmlSerializers[type];
        }

        /// <summary>
        /// Determines whether or not a particular type is a data contract
        /// </summary>
        /// <param name="type">The type to check</param>
        /// <returns>true if the type is a data contract, false otherwise</returns>
        private static bool IsDataContract(Type type)
        {
            if(KnownTypeNatures.ContainsKey(type))
            {
                return KnownTypeNatures[type];
            }

            var hasDcAtt = type.GetCustomAttributes(typeof (DataContractAttribute), false).Length > 0;
            return KnownTypeNatures[type] = hasDcAtt;
        }

        /// <summary>
        /// Serializes a data contract
        /// </summary>
        /// <param name="parentNs">The namespace the output element should belong to</param>
        /// <param name="param">The parameter for the method the object is destined for</param>
        /// <param name="data">The value of the object</param>
        /// <returns>An <see cref="XElement"/> that represents passing the specified value into the specified parameter</returns>
        /// <remarks>Not sure if this will work for formats other than SOAP 1.2</remarks>
        private static XElement SerializeDataContract(string parentNs, ParameterInfo param, object data)
        {
            var from = param.ParameterType;
            var fqtn = from.FullName;
            var dcNsSuffix = fqtn.Substring(0, fqtn.LastIndexOf('.'));
            var dcNs = DcNsPrefix + dcNsSuffix;
            var wrapper = new XElement(XName.Get("message", parentNs));
            wrapper.SetAttributeValue(XName.Get("b", XNamespace.Xmlns.NamespaceName), dcNs);
            wrapper.SetAttributeValue(XName.Get("i", XNamespace.Xmlns.NamespaceName), Xsi);
            
            if (data == null)
            {
                wrapper.SetAttributeValue(XName.Get("nil", Xsi), true);
                return wrapper;
            }

            var serializer = GetDataContractSerializer(from);
            var stream = new MemoryStream();
            serializer.WriteObject(stream, data);
            stream.Position = 0;
            var root = XElement.Load(stream);
            var elements = root.Elements();

            foreach(var element in elements)
            {
                var newElem = new XElement(XName.Get(element.Name.LocalName, dcNs));

                foreach(var attribute in newElem.Attributes())
                {
                    newElem.SetAttributeValue(attribute.Name, attribute.Value);
                }

                newElem.Add(element.Nodes());
                wrapper.Add(newElem);
            }

            return wrapper;
        }

        /// <summary>
        /// Serializes a primitive (more accurately a non-data contract)
        /// </summary>
        /// <param name="parentNs">The namespace the output element should belong to</param>
        /// <param name="param">The parameter for the method the object is destined for</param>
        /// <param name="data">The value of the object</param>
        /// <returns>An <see cref="XElement"/> that represents passing the specified value into the specified parameter</returns>
        /// <remarks>Not sure if this will work for formats other than SOAP 1.2</remarks>
        private static XElement SerializePrimitive(string parentNs, ParameterInfo param, object data)
        {
            var name = param.Name;
            var element = new XElement(XName.Get(name, parentNs));

            if (data != null)
            {
                var serializer = GetXmlSerializer(param.ParameterType);
                var stringBuilder = new StringBuilder();
                var stringWriter = new StringWriter(stringBuilder);
                serializer.Serialize(stringWriter, data);

                var doc = XElement.Parse(stringBuilder.ToString());
                element.Add(doc.Nodes());
            }
            else
            {
                element.SetAttributeValue(XName.Get("nil", Xsi), true);
            }

            return element;
        }


        /// <summary>
        /// Takes a <see cref="Message"/> that has (presumably) been received from the server and performs the requested call on the callback object
        /// </summary>
        /// <param name="callbackClass">The instance of the callback contract that calls should be made on</param>
        /// <param name="callbackType">The type of the callback object</param>
        /// <param name="parcel">The <see cref="Message"/> to perform the action described in</param>
        internal static void PerformCallback(object callbackClass, Type callbackType, Message parcel)
        {
            var action = parcel.Headers.Action.Split(new[] { '/' }).Last();

            if (MessageDeserializers.ContainsKey(action))
            {
                MessageDeserializers[action](parcel);
                return;
            }
            
            var method = callbackType.GetMethod(action);

            if (method == null)
            {
                MessageDeserializers[action] = msg => { };
                return;
            }

            var methodParams = method.GetParameters();
            var callBuildSteps = new List<Func<XElement, object>>();

            foreach (var parameter in methodParams)
            {
                var parameterType = parameter.ParameterType;
                var dataContractAttributeCount = parameterType.GetCustomAttributes(typeof (DataContractAttribute), false).Length;
                
                if(dataContractAttributeCount == 0)
                {
                    callBuildSteps.Add(d => DeserializePrimitive(parameterType, d.Value));
                }
                else
                {
                    callBuildSteps.Add(d => DeserializeDataContract(parameterType, d));
                }
            }

            var messageDeserializer = new Action<Message>(msg =>
            {
                var parameters = GetMethodParameters(msg, action);
                var @params = parameters.Select((x, i) => callBuildSteps[i](x)).ToArray();
                method.Invoke(callbackClass, @params);
            });

            (MessageDeserializers[action] = messageDeserializer)(parcel);
        }

        /// <summary>
        /// Prepares a message representing a method call at the server
        /// </summary>
        /// <typeparam name="T">The type of the service contract</typeparam>
        /// <param name="callDescriptor">A <see cref="MethodCallExpression"/> indicating what method (and with what parameters) should be called</param>
        /// <param name="messageVersion">The way the <see cref="Message"/> that is returned should be formatted</param>
        /// <returns>A <see cref="Message"/> describing the requested action</returns>
        internal static Message PrepareMessage<T>(Expression<Action<T>> callDescriptor, MessageVersion messageVersion)
        {
            var method = callDescriptor.Body as MethodCallExpression;

            if(method == null)
            {
                return null;
            }

            var @params = method.Arguments.Select(x => Expression.Lambda(x).Compile().DynamicInvoke()).ToArray();

            if (MessageSerializers.ContainsKey(method.Method))
            {
                return MessageSerializers[method.Method](@params);
            }

            var paramInfos = method.Method.GetParameters();
            var paramCount = @params.Length;
            var clientType = typeof(T);
            var sc = (ServiceContractAttribute)clientType.GetCustomAttributes(typeof(ServiceContractAttribute), false).First();
            var ns = (sc.Namespace ?? "http://tempuri.org").TrimEnd('/') + "/";
            var svc = (ns + clientType.Name);
            var act = svc + "/" + method.Method.Name;
            var serializeSteps = new List<Func<object, XElement>>();

            for (var i = 0; i < paramCount; i++)
            {
                var info = paramInfos[i];
                if(IsDataContract(paramInfos[i].ParameterType))
                {
                    serializeSteps.Add(param => SerializeDataContract(ns, info, param));
                }
                else
                {
                    serializeSteps.Add(param => SerializePrimitive(ns, info, param));
                }
            }

            var scheme = new Func<object[], Message>(parameters =>
            {
                var nodes = parameters.Select((x, i) => serializeSteps[i](x)).ToArray();
                return Message.CreateMessage(messageVersion, act, new CustomBodyWriter(clientType, method.Method, nodes));
            });

            return (MessageSerializers[method.Method] = scheme)(@params);
        }
    }
}
